package service

import (
	"encoding/json"
	"errors"
	"gitee.com/ling1314/origin-utility/constant"
	"gitee.com/ling1314/origin-utility/guacd"
	"gitee.com/ling1314/origin-utility/miao_ser"
	"gitee.com/ling1314/origin-utility/model"
	"gitee.com/ling1314/origin-utility/modules/repository"
	"gitee.com/ling1314/origin-utility/utils"
	"github.com/gorilla/websocket"
	log "github.com/sirupsen/logrus"
	"net/url"
	"sync"
	"time"
)

var mutex sync.Mutex

type sessionService struct {
}

func (ser sessionService) SendInstruct(sessionId int64, title, msg, msgType string) (err error) {
	return ser.WriteHintMessage(sessionId, title, msg, msgType)
}

func (ser sessionService) Logoff(id int64) (err error) {
	var (
		s model.HostSession
	)
	if s, err = repository.HostSessionRepository.FindById(id); err != nil {
		return
	}
	ser.CloseSessionById(s.HostSessionId, "管理员强制注销了你的会话")
	s.Status = model.VaSessionStatusForWinDestroying
	s.HoldStatus = model.VaSessionHoldForHeld
	s.DisconnectedTime = time.Now().Unix()
	s.Message = "管理员强制注销了会话"
	if err = repository.HostSessionRepository.UpdateById(&s); err != nil {
		return err
	}
	body, _ := json.Marshal(model.LogoffSession{
		HostSessionId: s.HostSessionId,
		WinSessionId:  int64(s.WinSessionId),
		Result:        1,
	})
	msg := model.ClientInstruct{
		Code:      constant.LogoffSessionForInform,
		ServeTime: time.Now().Unix(),
		Body:      string(body),
	}
	err = miao_ser.SendInstruct(s.HostId, msg)
	return err
}

func (ser sessionService) Break(id int64) (err error) {
	var (
		s model.HostSession
	)
	if s, err = repository.HostSessionRepository.FindById(id); err != nil {
		return
	}
	if s.HoldStatus != model.VaSessionHoldForHolding {
		err = errors.New("只有处于使用中的会话才能被关闭")
		return
	}
	s.Status = model.VaSessionStatusForWsBreak
	s.Message = "管理员强制关闭了会话"
	if err = repository.HostSessionRepository.UpdateById(&s); err != nil {
		return err
	}
	ser.CloseSessionById(id, "管理员强制关闭了您的会话")
	return
}

func (ser sessionService) GetSessionPageForAdmin(userId int64, input model.AdminSearchHostSession) (pr model.PageResult, err error) {
	var (
		userInfo *model.User
		li       []model.HostSession
		total    int64
	)
	if userInfo, err = repository.UserRepository.FindById(userId); err != nil {
		return
	}
	if userInfo.AccountLv == model.AccountLvForUser {
		err = errors.New("权限不足")
		return
	}
	if li, total, err = repository.HostSessionRepository.FindPage(input); err != nil {
		return
	}
	pr.List = li
	pr.Total = total
	return
}

func (ser sessionService) CreateSession(userId, appId int64) (rs model.CheckSessionReuseOutput, err error) {
	var (
		appInfo  model.AppIssue
		session  *model.HostSession
		clu      model.Cluster
		userInfo *model.User
		host     model.AppHost
	)
	if userInfo, err = repository.UserRepository.FindById(userId); err != nil {
		return
	}
	if appInfo, err = repository.AppRepository.FindAppIssueById(appId); err != nil {
		return
	}
	if clu, err = repository.ClusterRepository.FindById(appInfo.ClusterId); err != nil {
		return
	}
	if host, err = ser.SelectHostWhenIgnoreReuseSession(appInfo, clu, userId); err != nil {
		return
	}
	session, err = ser.newHostSession("rdp", host, userInfo, clu, "", "web")
	if err != nil {
		return
	}
	rs = model.CheckSessionReuseOutput{
		HostSessionId: session.HostSessionId,
		Possibility:   false,
	}
	return
}

func (ser sessionService) InsertAppOpenLog(hostSessionId int64, appId int64) (err error) {
	var (
		hs   model.HostSession
		appI model.AppIssue
	)
	if hs, err = repository.HostSessionRepository.FindById(hostSessionId); err != nil {
		return
	}
	if appI, err = repository.AppRepository.FindAppIssueById(appId); err != nil {
		return
	}
	l := model.MapHostSessionForApp{
		LogId:         utils.GetSoftId(),
		HostSessionId: hostSessionId,
		HostId:        hs.HostId,
		AppId:         appId,
		AppName:       appI.AppName,
		Icon:          appI.Icon,
		ClusterId:     appI.ClusterId,
		ClusterName:   hs.ClusterName,
		CreatedAt:     time.Now().Unix(),
	}
	if err = repository.MapHostSessionForAppRepository.Insert(&l); err != nil {
		return
	}
	return err
}

func (ser sessionService) BindWinSessionId(hostSessionId int64, winSessionId int) (err error) {
	var (
		s model.HostSession
	)
	if s, err = repository.HostSessionRepository.FindById(hostSessionId); err != nil {
		return
	}
	s.WinSessionId = winSessionId
	s.Status = model.VaSessionStatusForBindWinId
	s.Message = "会话使用中"
	if err = repository.HostSessionRepository.UpdateById(&s); err != nil {
		return
	}
	return
}

func (ser sessionService) CheckSessionReuse(userId, appId int64) (rs model.CheckSessionReuseOutput, err error) {
	var (
		li       []model.HostSession
		appLogs  []model.MapHostSessionForApp
		appInfo  model.AppIssue
		clu      model.Cluster
		session  *model.HostSession
		userInfo *model.User
	)
	if userInfo, err = repository.UserRepository.FindById(userId); err != nil {
		return
	}
	if appInfo, err = repository.AppRepository.FindAppIssueById(appId); err != nil {
		return
	}
	if li, err = repository.HostSessionRepository.FindUserActionSession(userId, appInfo.ClusterId); err != nil {
		return
	}
	clu, err = repository.ClusterRepository.FindById(appInfo.ClusterId)
	if err != nil {
		return
	}
	// 检查会话，情况一：没有可复用的会话，挑选主机，返回会话id
	if len(li) == 0 {
		var host model.AppHost
		if host, err = ser.SelectHostWhenIgnoreReuseSession(appInfo, clu, userId); err != nil {
			return
		}
		session, err = ser.newHostSession("rdp", host, userInfo, clu, "", "web")
		if err != nil {
			return
		}
		rs = model.CheckSessionReuseOutput{
			HostSessionId: session.HostSessionId,
			Possibility:   false,
		}
		return
	}
	// 检查会话中是否打开了这个应用
	sIds := make([]int64, len(li))
	for i := range li {
		sIds[i] = li[i].HostSessionId
	}
	if appLogs, err = repository.MapHostSessionForAppRepository.FindListBySessionIdsAndAppId(sIds, appId); err != nil {
		return
	}
	// 检查会话，情况二：已有关于这个应用的会话，提示可以复用会话
	if len(appLogs) > 0 {
		rs = model.CheckSessionReuseOutput{
			HostSessionId: appLogs[0].HostSessionId,
			Possibility:   true,
		}
		return
	}
	// 检查会话，情况三：有会话，但是不是这个应用，检查是否具备复用可能性
	var (
		sid   int64
		maybe bool
	)
	sid, maybe, err = ser.SelectHostWhenReuseSession(li, appInfo, clu, userInfo)
	if err != nil {
		return
	}
	rs = model.CheckSessionReuseOutput{
		HostSessionId: sid,
		Possibility:   maybe,
	}
	return
}

func (ser sessionService) Resize(width, height string, sessionId int64) (err error) {
	if len(width) == 0 || len(height) == 0 {
		return errors.New("参数异常")
	}
	var s model.HostSession
	if s, err = repository.HostSessionRepository.FindById(sessionId); err != nil {
		return err
	}
	s.Width = width
	s.Height = height
	if err = repository.HostSessionRepository.UpdateById(&s); err != nil {
		return err
	}
	return nil
}

//func (ser sessionService) SessionDisconnect(sessionId int64) {
//	ser.CloseSessionById(sessionId, guacd.ForcedBreak, "管理员强制关闭了此会话")
//}

// ================= 工具方法 =================
func (ser sessionService) newHostSession(proto string, hostInfo model.AppHost, userInfo *model.User, cluInfo model.Cluster, msg string, clientType string) (m *model.HostSession, err error) {
	// 创建会话记录
	m = &model.HostSession{
		HostSessionId: utils.GetSoftId(),
		Protocol:      proto,
		HostId:        hostInfo.HostId,
		HostIp:        hostInfo.IP,
		ConnectPort:   int(hostInfo.RdpPort),
		ConnectionId:  "",
		//WinAccount:       userInfo.Username,
		//WinPwd:           userInfo.Password,
		WinAccount:       "administrator",
		WinPwd:           "qwer1234!",
		WinSessionId:     0,
		Width:            "",
		Height:           "",
		Status:           model.VaSessionStatusForCreate,
		HoldStatus:       model.VaSessionHoldForInit,
		Message:          msg,
		ConnectedTime:    0,
		DisconnectedTime: 0,
		CreatedTime:      time.Now().Unix(),
		UserId:           userInfo.UserId,
		ClusterId:        cluInfo.ClusterId,
		ClusterName:      cluInfo.Name,
	}
	err = repository.HostSessionRepository.Create(m)
	return m, err
}

// CloseSessionById 关闭会话时通知另外一方也要断开链接，同时如果存在观察者，也要一并关闭观察者的会话
func (ser sessionService) CloseSessionById(sessionId int64, hint string) {
	mutex.Lock()
	defer mutex.Unlock()
	nextSession := guacd.GlobalSessionManager.GetById(sessionId)
	if nextSession != nil {
		log.Debugf("[%v] 会话关闭，原因：%v", sessionId, hint)
		// 通知 ws 关闭
		ser.WriteCloseMessage(nextSession.WebSocket, hint)
		if nextSession.Observer != nil {
			obs := nextSession.Observer.All()
			for _, ob := range obs {
				// 通知 所有 Observer 的 ws 关闭
				ser.WriteCloseMessage(ob.WebSocket, hint)
				log.Debugf("[%v] 强制踢出会话的观察者: %v", sessionId, ob.ID)
			}
		}
	}
}

func (ser sessionService) WriteCloseMessage(ws *websocket.Conn, hint string) {
	if ws != nil {
		// 第一个参数 是 reason ，第二次参数才是 code，guacamole-common.js 是这样写的，这里模仿它写
		hs := guacd.NewInstruction(constant.HintInstruction, url.QueryEscape("系统提示"), url.QueryEscape(hint), "error")
		_ = ws.WriteMessage(websocket.TextMessage, []byte(hs.String()))
		disconnect := guacd.NewInstruction("disconnect")
		_ = ws.WriteMessage(websocket.TextMessage, []byte(disconnect.String()))
	}
}

// WriteHintMessage 发送提示信息
func (ser sessionService) WriteHintMessage(hostSessionId int64, title, msg, msgType string) (err error) {
	mutex.Lock()
	defer mutex.Unlock()
	nextSession := guacd.GlobalSessionManager.GetById(hostSessionId)
	if nextSession == nil || nextSession.WebSocket == nil {
		err = errors.New("会话不在线")
		return err
	}
	if nextSession != nil {
		// 通知 ws 关闭
		if nextSession.WebSocket != nil {
			// 第一个参数 是 reason ，第二次参数才是 code，guacamole-common.js 是这样写的，这里模仿它写
			hs := guacd.NewInstruction(constant.HintInstruction, url.QueryEscape(title), url.QueryEscape(msg), msgType)
			_ = nextSession.WebSocket.WriteMessage(websocket.TextMessage, []byte(hs.String()))
		}

		if nextSession.Observer != nil {
			obs := nextSession.Observer.All()
			for _, ob := range obs {
				// 通知 所有 Observer 的 ws
				hs := guacd.NewInstruction(constant.HintInstruction, url.QueryEscape(title), url.QueryEscape(msg), msgType)
				_ = ob.WebSocket.WriteMessage(websocket.TextMessage, []byte(hs.String()))
			}
		}
	}
	return nil
}

// SelectHostWhenReuseSession 有可复用会话时挑选主机，受到单例应用的影响，即使存在会话，也不一定能复用，这里检查的是可能性,如果会话不能复用回自动创建一个会话
func (ser sessionService) SelectHostWhenReuseSession(sessionList []model.HostSession, appInfo model.AppIssue, clu model.Cluster, userInfo *model.User) (sessionId int64, b bool, err error) {
	switch clu.ClusterType {
	case model.ClusterTypeForPrivate:
		return sessionList[0].HostSessionId, true, nil
	case model.ClusterTypeForPreempt:
		return sessionList[0].HostSessionId, true, nil
	case model.ClusterTypeForShare:
		if !appInfo.IsSingle {
			// 从自己的会话中复用一个会话
			return sessionList[0].HostSessionId, true, nil
		} else {
			// 检查主机上是否只有自己一个人，如果有就返回这台主机。
			// 否则就挑选一台没有开启过这个应用的主机
			var host model.AppHost
			sessionId, b, host, err = ser.selectHostForSingleApp(sessionList, clu.ClusterId, appInfo.AppId)
			if err != nil {
				return
			}
			// 说明复用会话命中了，返回会话id和true
			if sessionId != 0 || b {
				return
			}
			var session *model.HostSession
			session, err = ser.newHostSession("rdp", host, userInfo, clu, "", "web")
			if err != nil {
				return
			}
			sessionId = session.HostSessionId
			b = false
			return
		}
	default:
		return 0, false, errors.New("未知的集群类型")
	}
}

// SelectHostWhenIgnoreReuseSession 当没有可复用会话时挑选主机,如果有会话复用的情况请使用 SelectHostWhenReuseSession().
// 是否需要考虑，在调用之前自己检查
func (ser sessionService) SelectHostWhenIgnoreReuseSession(app model.AppIssue, clu model.Cluster, userId int64) (host model.AppHost, err error) {
	switch clu.ClusterType {
	case model.ClusterTypeForPrivate:
		host, err = ser.selectPrivateHost(userId, clu.ClusterId)
	case model.ClusterTypeForPreempt:
		host, err = ser.selectPreemptHost(userId, clu.ClusterId)
	case model.ClusterTypeForShare:
		host, err = ser.selectShareHost(app, clu.ClusterId)
	}
	return
}

// selectPrivateHost 挑选私有主机（不考虑会话复用，外层自己检测），先检查用户在该集群下是否已有私有主机，没有就挑选一台没有会话的然后绑定给公户
func (ser sessionService) selectPrivateHost(userId int64, clusterId int64) (host model.AppHost, err error) {
	var (
		li []model.AppHost
	)
	if host, err = repository.HostRepository.FindPrivateHost(userId, clusterId); err != nil {
		return
	}
	if host.HostId != 0 {
		return
	}
	if li, err = repository.HostRepository.SelectZeroSessionHost(clusterId); err != nil {
		return
	}
	if len(li) == 0 {
		err = errors.New("没有空闲主机可以分配了")
		return
	}
	host = li[0]
	host.BindUserID = userId
	if err = repository.HostRepository.UpdateById(&host); err != nil {
		return
	}
	return
}

// selectPreemptHost 挑选抢占主机（不考虑会话复用，外层自己检测）。从集群下挑选出一台没有任何活跃会话也没有被人绑定或者锁定的主机，命中后锁定该主机
func (ser sessionService) selectPreemptHost(userId int64, clusterId int64) (host model.AppHost, err error) {
	var (
		li []model.AppHost
	)
	if li, err = repository.HostRepository.SelectZeroSessionHost(clusterId); err != nil {
		return
	}
	if len(li) == 0 {
		err = errors.New("没有空闲主机可以分配了")
		return
	}
	host = li[0]
	host.LockedUserID = userId
	if err = repository.HostRepository.UpdateById(&host); err != nil {
		return
	}
	return
}

// selectShareHost 挑选共享主机（不考虑会话复用，外层自己检测），只考虑单例应用问题
func (ser sessionService) selectShareHost(app model.AppIssue, clusterId int64) (host model.AppHost, err error) {
	var (
		hosts   = make([]model.AppHost, 0)
		allHost []model.AppHost
	)
	allHost, err = repository.HostRepository.FindWithClusterId(clusterId)
	if err != nil {
		return
	}
	if !app.IsSingle {
		hosts = allHost
	} else {
		for i := range allHost {
			var num int64
			num, err = repository.MapHostSessionForAppRepository.SelectAppStartingTimesForActionSession(allHost[i].HostId, app.AppId)
			if err != nil {
				log.Error(err)
				return
			}
			if num == 0 {
				hosts = append(hosts, allHost[i])
			}
		}
	}

	// 如果没有符合要求的主机就返回 0，false,0
	if len(hosts) == 0 {
		err = errors.New("没有空闲的主机")
		return
	}
	// 否则就挑选一台负载最小的主机
	host = ser.SelectMin(hosts)
	return
}

// SelectMin
// todo 挑选出负载最小的主机
func (ser sessionService) SelectMin(li []model.AppHost) (m model.AppHost) {
	return li[0]
}

// selectMonopolizeHost 考虑会话复用时挑选出符合单例应用的主机，
// 将单例应用理解为公交车的副驾驶座位(只有一个的特殊座位)，公交车相当于主机。
// 如果已经关联主机上这个座位是空的，就直接命中。会话可复用
// 否则就从集群下的其他主机上挑选特殊座位空闲的。能命中就是有主机单不可复用，不能命中就是 无可用主机
// sessionList 必须是用户自己的活跃会话，调用方法时自己控制
func (ser sessionService) selectHostForSingleApp(sessionList []model.HostSession, clusterId int64, appId int64) (sessionId int64, b bool, host model.AppHost, err error) {
	hostIds := make([]int64, 0)
	// 检查用户已有的会话主机是否具备复用的可能性
	for i := range sessionList {
		if !utils.Contains(hostIds, sessionList[i].HostId) {
			var num int64
			num, err = repository.MapHostSessionForAppRepository.SelectAppStartingTimesForActionSession(sessionList[i].HostId, appId)
			if err != nil {
				return
			}
			if num == 0 {
				sessionId = sessionList[i].HostSessionId
				b = true
				return
			}
			hostIds = append(hostIds, sessionList[i].HostId)
		}
	}
	// 没有复用可能性就检查集群下哪些主机符合要求(副驾驶位置还空着)
	hosts := make([]model.AppHost, 0)
	var li []model.AppHost
	li, err = repository.HostRepository.FindWithClusterId(clusterId)
	if err != nil {
		return
	}
	for i := range li {
		if !utils.Contains(hostIds, li[i].HostId) {
			var num int64
			num, err = repository.MapHostSessionForAppRepository.SelectAppStartingTimesForActionSession(sessionList[i].HostId, appId)
			if err != nil {
				return
			}
			if num == 0 {
				hosts = append(hosts, li[i])
			}
		}
	}
	// 如果没有符合要求的主机就返回 0，false,0
	if len(hosts) == 0 {
		err = errors.New("没有可用的主机")
		return
	}
	// 否则就挑选一台负载最小的主机
	host = ser.SelectMin(hosts)
	b = false
	return
}
